package com.jintian.smart.kernel.switching.runtime.db;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.jintian.smart.kernel.common.util.ObjectUtil;
import com.jintian.smart.kernel.common.util.StringUtil;
import com.jintian.smart.kernel.core.expr.IExpressionCalc;
import com.jintian.smart.kernel.switching.api.IExtractor;
import com.jintian.smart.kernel.switching.api.IFileAdapter;
import com.jintian.smart.kernel.switching.db.ExtractFileAdapter;
import com.jintian.smart.kernel.switching.db.ExtractRule;
import com.jintian.smart.kernel.switching.db.ExtractTable;
import com.jintian.smart.kernel.switching.db.RuleParameter;
import com.jintian.smart.kernel.switching.dto.db.Attachment;
import com.jintian.smart.kernel.switching.dto.db.DBDataPacket;
import com.jintian.smart.kernel.switching.entity.TableDMLLog;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.model.ZipParameters;
import org.apache.commons.io.FileUtils;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectOutput;
import org.beetl.sql.clazz.kit.AutoSQLEnum;
import org.beetl.sql.core.SQLBatchReady;
import org.beetl.sql.core.SQLExecutor;
import org.beetl.sql.core.SQLManager;
import org.beetl.sql.core.SqlId;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;

@SuppressWarnings("rawtypes")
public class SimpleDBExtractor implements IExtractor, ApplicationContextAware {
    public static final String FILE_DATA_DUMP = "data.dump";
    public static final String DIR_ATTACHMENT = "attachment";
    private final SQLManager sqlManager;

    private final ObjectMapper objectMapper;

    @Value("${mda.switching.rule-path}")
    private String rulePath;

    @Autowired
    private IExpressionCalc expressionCalc;

    @Autowired
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void setExpressionCalc(IExpressionCalc expressionCalc) {
        this.expressionCalc = expressionCalc;
    }

    public SimpleDBExtractor(SQLManager sqlManager) {

        this.sqlManager = sqlManager;
        JavaTimeModule module = new JavaTimeModule();
        LocalDateTimeDeserializer dateTimeDeserializer = new LocalDateTimeDeserializer(
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        LocalDateTimeSerializer dateTimeSerializer = new LocalDateTimeSerializer(
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        module.addDeserializer(LocalDateTime.class, dateTimeDeserializer);
        module.addSerializer(LocalDateTime.class, dateTimeSerializer);
        this.objectMapper = Jackson2ObjectMapperBuilder.json()
                .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).locale(Locale.CHINA)
                .timeZone(TimeZone.getTimeZone("GMT+8")).modules(module).build();

    }

    private File getTempExchangeDir() {
        return new File(FileUtils.getTempDirectory(), UUID.randomUUID().toString());
    }

    private ClassLoader getClassLoader() {
        return this.applicationContext.getClassLoader();
    }

    @Override
    public boolean support(Class<?> dataType, Class<?> ruleClass, String ruleName) {
        return dataType.equals(DBDataPacket.class) && ExtractRule.class.equals(ruleClass) && this.applicationContext
                .getResource("classpath:etl/" + this.rulePath + "/" + ruleName + ".xml").exists();
    }

    @Override
    public File extract(Class<?> dataType, Object rule, String ruleName, Map<String, Object> variables)
            throws IOException {
        ExtractRule extractRule = (ExtractRule) rule;
        if (variables == null) {
            variables = new HashMap<>();
        }
        for (RuleParameter param : extractRule.getParameters()) {
            if (param.isRequired() && StringUtil.isBlank(param.getCalcExpr())
                    && !variables.containsKey(param.getName())) {
                throw new IllegalArgumentException("缺少参数" + (param.getTitle() == null ? param.getName() : param.getTitle()));
            }
            if (!variables.containsKey(param.getName()) && StringUtil.isNoneBlank(param.getCalcExpr())) {
                variables.put(param.getName(), expressionCalc.calculate(param.getCalcExpr(), variables));
            }
        }
        ClassLoader old = Thread.currentThread().getContextClassLoader();
        File tempDir = this.getTempExchangeDir();
        try {
            ClassLoader clsLoader = this.getClassLoader();
            Thread.currentThread().setContextClassLoader(clsLoader);
            DBDataPacket datapacket = new DBDataPacket(extractRule);

            for (ExtractTable table : extractRule.getTables()) {
                // 加载upsert数据
                datapacket.getUpsertData().putAll(this.loadTableData(clsLoader, null, table, variables));
                // 加载delete数据
                if (table.isLoadDeleted()) {
                    datapacket.getDeleteData().putAll(this.loadDeleteData(table));
                }
            }
            Map<String, List<Attachment>> attachmentMap = datapacket.getAttachments();
            // 加载关联文件
            for (ExtractFileAdapter fileAdapter : extractRule.getFileAdapters()) {
                // 循环关联数据执行下载
                for (ExtractFileAdapter.RefTable refTable : fileAdapter.getRefTables()) {
                    List refDataList = datapacket.getUpsertData(refTable.getRef());
                    if (ObjectUtil.isEmpty(refDataList)) {
                        continue;
                    }
                    List<Attachment> attachments = attachmentMap.get(refTable.getRef());
                    if (attachments == null) {
                        attachments = new ArrayList<>();
                        attachmentMap.put(refTable.getRef(), attachments);
                    }
                    attachments.addAll(this.downloadAttachment(fileAdapter, new File(tempDir, DIR_ATTACHMENT), refTable, refDataList));
                }
            }
            this.saveDataPacket(new File(tempDir, FILE_DATA_DUMP), datapacket);

            File zipFile = new File(FileUtils.getTempDirectory(), tempDir.getName() + ".zip");
            ZipFile zip = new ZipFile(zipFile);
            try {
                ZipParameters parameters = new ZipParameters();
                parameters.setIncludeRootFolder(false);
                zip.addFolder(tempDir, parameters);
                zip.close();
            } catch (Exception e) {
                throw new IllegalArgumentException(e);
            }
            return zipFile;
        } finally {
            try {
                FileUtils.deleteDirectory(tempDir);
            } catch (IOException e) {
                e.printStackTrace();
            }
            Thread.currentThread().setContextClassLoader(old);
        }
    }

    private void saveDataPacket(File file, DBDataPacket datapacket) {
        file.getParentFile().mkdirs();

        try (OutputStream out = new FileOutputStream(file)) {
            Hessian2ObjectOutput output = new Hessian2ObjectOutput(out);
            output.writeObject(datapacket);
            output.flushBuffer();
        } catch (Exception e) {
            throw new IllegalArgumentException(e.getMessage());
        }

    }

    private IFileAdapter getFileAdapter(Class storageBeanClass) {
        ApplicationContext app = this.applicationContext;
        while (app != null) {
            Collection<IFileAdapter> list = app.getBeansOfType(IFileAdapter.class).values();
            for (IFileAdapter IFileAdapter : list) {
                if (IFileAdapter.support(storageBeanClass)) {
                    return IFileAdapter;
                }
            }
            app = app.getParent();
        }

        throw new IllegalArgumentException("未注册支持" + storageBeanClass + "的FileAdapter");
    }

    private List<Attachment> downloadAttachment(ExtractFileAdapter fileAdapter, File attachmentDir, ExtractFileAdapter.RefTable refTable, List refDataList) {
        try {
            IFileAdapter instance = this.getFileAdapter(refDataList.get(0).getClass());
            List<Attachment> ret = new ArrayList<>();
            // 下载目录
            File downloadDir = attachmentDir;
            if (StringUtil.isNotBlank(fileAdapter.getDir())) {
                downloadDir = new File(downloadDir, fileAdapter.getDir());
            }
            if (StringUtil.isNotBlank(refTable.getDir())) {
                downloadDir = new File(downloadDir, refTable.getDir());
            }
            for (Object obj : refDataList) {
                Attachment attachment = instance.download(obj, downloadDir);
                if (attachment != null) {
                    ret.add(attachment);
                }
            }
            return ret;
        } catch (Exception e) {
            throw new IllegalArgumentException(e.getMessage());
        }
    }

    private Map<String, List<String>> loadDeleteData(ExtractTable table) {
        // 读取删除的key值
        String tableName = table.getName().toUpperCase();
        Map<String, List<String>> ret = new HashMap<>();
        Map<String, Object> paras = new HashMap<>();
        paras.put("tableName", tableName);

        List<TableDMLLog> logs = this.sqlManager.execute(
                "select * from MDA_TableDMLLog where tableName=#{tableName} and operation='DELETE'", TableDMLLog.class,
                paras);
        List<String> keyValues = logs.stream().map(e -> e.getKeyValues()).collect(Collectors.toList());
        ret.put(tableName, keyValues);

        // 删除已读取的key值
        List<Object[]> batchArgs = logs.stream().map(e -> new Object[]{e.getGuid()}).collect(Collectors.toList());
        SQLBatchReady batch = new SQLBatchReady("delete from MDA_TableDMLLog where guid=?", batchArgs);
        this.sqlManager.executeBatchUpdate(batch);

        for (ExtractTable subTable : table.getChildren()) {
            ret.putAll(this.loadDeleteData(subTable));
        }
        return ret;
    }

    private Map<String, List> loadTableData(ClassLoader clsLoader, ExtractTable parent, ExtractTable table,
                                            Map<String, Object> variables) {
        Map<String, List> ret = new HashMap<>();
        Class<?> clazz;
        try {
            clazz = clsLoader.loadClass(table.getClazz());
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException(e.getMessage());
        }
        String idAttr = this.sqlManager.getClassDesc(clazz).getIdAttr();
        List<?> listData = null;
        String sqlId = table.getSqlId();
        if (StringUtil.isNotBlank(sqlId)) {
            // SQLID 加载数据
            String namespace = sqlId.substring(0, sqlId.lastIndexOf("."));
            String id = sqlId.substring(sqlId.lastIndexOf(".") + 1);
            SqlId sql = SqlId.of(namespace, id);
            listData = sqlManager.select(sql, clazz, variables);
        } else {
            SQLExecutor script = sqlManager.getScript(clazz, AutoSQLEnum.SELECT_BY_TEMPLATE);
            listData = script.select(clazz, variables);
        }
        if ((table.getMinOccurs() > 0 && listData.size() < table.getMinOccurs()) || table.getMaxOccurs() > 0 && listData.size() > table.getMaxOccurs()) {
            throw new IllegalArgumentException(table.getName() + "数据条数要求[" + table.getMinOccurs() + ","
                    + table.getMaxOccurs() + "]范围内，实际条数" + listData.size());
        }
        ret.put(table.getName().toUpperCase(), listData);
        // 加载子表数据
        if (listData != null && listData.size() > 0)
            for (int i = 0; i < listData.size(); i++) {
                Object data = listData.get(i);
                @SuppressWarnings("unchecked")
                Map<String, Object> dataMap = this.objectMapper.convertValue(data, HashMap.class);
                // 级联加载
                List<ExtractTable> subTables = table.getChildren();
                if (subTables == null || subTables.size() <= 0) {
                    continue;
                }
                for (ExtractTable subTable : subTables) {
                    // 变量准备
                    Map<String, Object> subLoadVariables = new HashMap<>();
                    if (subTable.getForeignKey() != null) {
                        String[] foreignKeys = subTable.getForeignKey().split(";");
                        for (String foreignKey : foreignKeys) {
                            if (StringUtil.isBlank(foreignKey))
                                continue;
                            Object parentValue = null;
                            String[] keys = foreignKey.split("=");
                            String parentAttr = keys.length > 1 ? keys[1].trim() : idAttr;
                            String subAttr = keys[0].trim();
                            parentValue = dataMap.get(parentAttr);
                            subLoadVariables.put(subAttr, parentValue);
                        }
                    } else {
                        dataMap.forEach((key, value) -> {
                            subLoadVariables.put(table.getName() + "." + key, value);
                        });
                    }

                    Map<String, List> subMap = this.loadTableData(clsLoader, table, subTable, subLoadVariables);
                    this.joinMap(subMap, ret);
                }
            }
        return ret;
    }

    @SuppressWarnings("unchecked")
    private void joinMap(Map<String, List> source, Map<String, List> target) {
        // 合并Map数据
        for (Entry<String, List> e : source.entrySet()) {
            List exitsList = target.get(e.getKey());
            if (exitsList == null) {
                target.put(e.getKey(), e.getValue());
            } else {
                exitsList.addAll(e.getValue());
            }
        }
    }
}
